package com.ryder.petmatediarybackend.module.auth.service.impl;

import cn.dev33.satoken.stp.SaLoginModel;
import cn.dev33.satoken.stp.SaTokenInfo;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.util.RandomUtil;
import com.ryder.petmatediarybackend.common.enums.DeviceTypeEnum;
import com.ryder.petmatediarybackend.common.exception.BusinessException;
import com.ryder.petmatediarybackend.common.util.LoginLogUtil;
import com.ryder.petmatediarybackend.common.util.RedisKeys;
import com.ryder.petmatediarybackend.common.util.RedisUtil;
import com.ryder.petmatediarybackend.module.auth.dto.LoginDTO;
import com.ryder.petmatediarybackend.module.auth.dto.RefreshTokenDTO;
import com.ryder.petmatediarybackend.module.auth.dto.RegisterDTO;
import com.ryder.petmatediarybackend.module.auth.service.AuthService;
import com.ryder.petmatediarybackend.module.auth.service.TokenService;
import com.ryder.petmatediarybackend.module.auth.vo.LoginVO;
import com.ryder.petmatediarybackend.module.auth.vo.TokenVO;
import com.ryder.petmatediarybackend.module.auth.vo.UserInfoVO;
import com.ryder.petmatediarybackend.module.auth.vo.UserVO;
import com.ryder.petmatediarybackend.module.log.entity.SysLoginLog;
import com.ryder.petmatediarybackend.module.log.service.SysLoginLogService;
import com.ryder.petmatediarybackend.module.pet.entity.SysUser;
import com.ryder.petmatediarybackend.module.pet.service.SysUserService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 描述: 认证服务实现类（Sa-Token 双 Token 模式）
 *
 * @author Ryder
 * @version 1.0
 * @since 1.0
 * 更新日期: 2025/10/27
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

    private final SysUserService sysUserService;
    private final PasswordEncoder passwordEncoder;
    private final TokenService tokenService;
    private final RedisUtil redisUtil;
    private final SysLoginLogService loginLogService;

    @Override
    public UserVO register(RegisterDTO dto) {
        // 调用 SysUserService 创建用户并绑定默认角色
        return sysUserService.createUser(dto);
    }

    @Override
    public LoginVO login(LoginDTO dto, HttpServletRequest request) {
        String userAgent = request.getHeader("User-Agent");
        
        // 0. 严格校验设备类型
        if (!DeviceTypeEnum.isValid(dto.getDeviceType())) {
            throw new BusinessException("设备类型无效");
        }
        
        DeviceTypeEnum deviceType = DeviceTypeEnum.fromCode(dto.getDeviceType());
        
        try {
            // 1. 根据用户名查询用户
            SysUser user = sysUserService.getUserByUsername(dto.getUsername());
            if (user == null) {
                // 记录登录失败日志
                recordLoginLog(null, deviceType, 0, "用户不存在", request, userAgent);
                throw new BusinessException("用户名或密码错误");
            }

            // 2. 验证密码
            if (!passwordEncoder.matches(dto.getPassword(), user.getPasswordHash())) {
                // 记录登录失败日志
                recordLoginLog(user.getId(), deviceType, 0, "密码错误", request, userAgent);
                throw new BusinessException("用户名或密码错误");
            }

            // 3. 检查用户状态
            if (user.getStatus() == 0) {
                // 记录登录失败日志
                recordLoginLog(user.getId(), deviceType, 0, "账号已被禁用", request, userAgent);
                throw new BusinessException("账号已被禁用");
            }

            // 4. 踢掉该用户在同一设备类型的旧登录（单设备登录策略）
            kickoutOldLogin(user.getId(), dto.getDeviceType());

            // 5. Sa-Token 登录，生成 Access Token（30分钟）
            StpUtil.login(user.getId(), new SaLoginModel()
                    .setDevice(dto.getDeviceType())
                    .setTimeout(1800));

            // 6. 获取 Access Token 信息
            SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
            String accessToken = tokenInfo.getTokenValue();

            // 7. 手动生成 Refresh Token（高强度随机字符串，64位）
            String refreshToken = RandomUtil.randomString(64);

            // 8. 将 Refresh Token 存储到 Redis（7天有效）
            // Key1: userId:deviceType -> refreshToken
            String refreshKey = RedisKeys.buildRefreshTokenKey(user.getId(), dto.getDeviceType());
            redisUtil.set(refreshKey, refreshToken, 604800);
            
            // Key2: refreshToken -> userId:deviceType（用于反查）
            String reverseKey = "refresh_token:" + refreshToken;
            redisUtil.set(reverseKey, user.getId() + ":" + dto.getDeviceType(), 604800);

            // 9. 构建 TokenVO
            TokenVO token = TokenVO.builder()
                    .accessToken(accessToken)
                    .refreshToken(refreshToken)
                    .accessTokenTimeout(tokenInfo.getTokenTimeout())
                    .refreshTokenTimeout(604800L)
                    .loginDevice(dto.getDeviceType())
                    .build();

            // 10. 构建用户基础信息 VO（不包含敏感信息）
            UserInfoVO userInfo = UserInfoVO.builder()
                    .userId(user.getId())
                    .username(user.getUsername())
                    .nickname(user.getNickname())
                    .avatar(user.getAvatar())
                    .status(user.getStatus())
                    .build();

            // 11. 获取用户角色列表
            List<String> roles = StpUtil.getRoleList();

            // 12. 获取用户权限列表
            List<String> permissions = StpUtil.getPermissionList();

            // 13. 记录登录成功日志
            recordLoginLog(user.getId(), deviceType, 1, null, request, userAgent);

            log.info("用户登录成功：userId={}, username={}, device={}, ip={}", 
                user.getId(), user.getUsername(), dto.getDeviceType(), request.getRemoteAddr());

            // 14. 返回完整登录信息
            return LoginVO.builder()
                    .token(token)
                    .userInfo(userInfo)
                    .roles(roles)
                    .permissions(permissions)
                    .build();
                    
        } catch (BusinessException e) {
            // 业务异常直接抛出（已记录日志）
            throw e;
        } catch (Exception e) {
            // 其他异常记录日志
            recordLoginLog(null, deviceType, 0, "系统异常：" + e.getMessage(), request, userAgent);
            log.error("登录异常", e);
            throw new BusinessException("登录失败，请稍后重试");
        }
    }

    /**
     * 踢掉旧登录（单设备登录策略）
     */
    private void kickoutOldLogin(Long userId, String deviceType) {
        try {
            // 1. 检查 Redis 中是否存在该用户该设备的 Refresh Token（说明已登录）
            String oldRefreshKey = RedisKeys.buildRefreshTokenKey(userId, deviceType);
            String oldRefreshToken = redisUtil.get(oldRefreshKey);
            
            if (oldRefreshToken != null) {
                log.info("检测到用户在同设备类型已登录，准备踢掉旧登录：userId={}, device={}", userId, deviceType);
                
                // 2. 将旧的 Refresh Token 加入黑名单（防止被继续使用）
                tokenService.addToBlacklist(oldRefreshToken, 604800L);
                log.info("已将旧的 Refresh Token 加入黑名单：userId={}, device={}", userId, deviceType);
                
                // 3. 删除旧的 Refresh Token 及其反查 Key
                String oldReverseKey = "refresh_token:" + oldRefreshToken;
                redisUtil.del(oldReverseKey);
                redisUtil.del(oldRefreshKey);
                log.info("已删除旧的 Refresh Token：userId={}, device={}", userId, deviceType);
                
                // 4. 获取旧的 Access Token 并加入黑名单
                try {
                    // 尝试获取该用户该设备的旧 Access Token
                    String oldAccessToken = StpUtil.getTokenValueByLoginId(userId, deviceType);
                    if (oldAccessToken != null) {
                        // 获取 Token 剩余有效时间
                        long timeout = StpUtil.getTokenTimeout(oldAccessToken);
                        if (timeout > 0) {
                            tokenService.addToBlacklist(oldAccessToken, timeout);
                            log.info("已将旧的 Access Token 加入黑名单：userId={}, device={}", userId, deviceType);
                        }
                    }
                } catch (Exception e) {
                    log.debug("获取旧 Access Token 失败：{}", e.getMessage());
                }
                
                // 5. 强制注销该用户在该设备的登录（清除 Sa-Token 的会话）
                try {
                    StpUtil.logout(userId, deviceType);
                    log.info("已踢掉旧登录：userId={}, device={}", userId, deviceType);
                } catch (Exception e) {
                    // 如果用户未登录，logout 会抛异常，忽略即可
                    log.debug("注销旧登录时出现异常（可能用户未登录）：{}", e.getMessage());
                }
            }
        } catch (Exception e) {
            log.error("踢掉旧登录失败", e);
        }
    }

    /**
     * 记录登录日志（异步）
     */
    private void recordLoginLog(Long userId, DeviceTypeEnum deviceType, Integer loginStatus, 
                                String failReason, HttpServletRequest request, String userAgent) {
        try {
            SysLoginLog loginLog = LoginLogUtil.buildLoginLog(
                userId,
                deviceType,
                loginStatus,
                failReason,
                request,
                1,  // 登录方式：1=账号密码
                1,  // 登录来源：1=系统
                userAgent
            );
            loginLogService.save(loginLog);
        } catch (Exception e) {
            log.error("记录登录日志失败", e);
        }
    }

    @Override
    public TokenVO refreshToken(RefreshTokenDTO dto) {
        // 1. 严格校验设备类型
        if (!DeviceTypeEnum.isValid(dto.getDeviceType())) {
            throw new BusinessException("设备类型无效");
        }
        
        // 2. 获取 refreshToken 和 deviceType
        String refreshToken = dto.getRefreshToken();
        String deviceType = dto.getDeviceType();

        // 3. 通过 refreshToken 反查 userId（从 Redis）
        String reverseKey = "refresh_token:" + refreshToken;
        String userDeviceInfo = redisUtil.get(reverseKey);
        
        if (userDeviceInfo == null) {
            throw new BusinessException("Refresh Token 无效或已过期");
        }
        
        // 解析 userId 和 deviceType
        String[] parts = userDeviceInfo.split(":");
        if (parts.length != 2) {
            throw new BusinessException("Refresh Token 数据异常");
        }
        
        Long userId = Long.parseLong(parts[0]);
        String storedDeviceType = parts[1];
        
        // 验证设备类型是否匹配
        if (!storedDeviceType.equals(deviceType)) {
            throw new BusinessException("设备类型不匹配");
        }

        // 4. 从 Redis 查询存储的 refreshToken（双重验证）
        String refreshKey = RedisKeys.buildRefreshTokenKey(userId, deviceType);
        String storedRefreshToken = redisUtil.get(refreshKey);

        // 5. 验证 refreshToken 是否匹配
        if (storedRefreshToken == null || !storedRefreshToken.equals(refreshToken)) {
            throw new BusinessException("Refresh Token 无效或已过期");
        }

        // 6. 检查 Refresh Token 是否在黑名单
        if (tokenService.isInBlacklist(refreshToken)) {
            log.warn("Refresh Token 在黑名单中，拒绝刷新：userId={}, device={}", userId, deviceType);
            throw new BusinessException("Refresh Token 已失效，请重新登录");
        }

        // 7. 获取旧的 Access Token 并加入黑名单
        try {
            String oldAccessToken = StpUtil.getTokenValueByLoginId(userId, deviceType);
            if (oldAccessToken != null) {
                // 获取 Token 剩余有效时间
                long timeout = StpUtil.getTokenTimeout(oldAccessToken);
                if (timeout > 0) {
                    tokenService.addToBlacklist(oldAccessToken, timeout);
                    log.info("已将旧的 Access Token 加入黑名单：userId={}, device={}, timeout={}s", userId, deviceType, timeout);
                }
            }
        } catch (Exception e) {
            log.debug("获取旧 Access Token 失败：{}", e.getMessage());
        }

        // 8. 踢掉该用户在同一设备类型的旧登录（清除旧的 Sa-Token 会话）
        try {
            StpUtil.logout(userId, deviceType);
            log.info("已踢掉旧登录：userId={}, device={}", userId, deviceType);
        } catch (Exception e) {
            // 如果用户未登录，logout 会抛异常，忽略即可
            log.debug("注销旧登录时出现异常（可能用户未登录）：{}", e.getMessage());
        }

        // 9. 重新登录生成新的 Access Token（30分钟）
        StpUtil.login(userId, new SaLoginModel()
                .setDevice(deviceType)
                .setTimeout(1800));
        
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        String newAccessToken = tokenInfo.getTokenValue();

        // 10. 重新加载用户的角色和权限到 Sa-Token Session
        // Sa-Token 会自动调用 StpInterfaceImpl 的 getRoleList 和 getPermissionList
        // 但为了确保立即生效，手动触发一次
        List<String> roles = StpUtil.getRoleList();
        List<String> permissions = StpUtil.getPermissionList();
        log.info("已重新加载用户权限：userId={}, roles={}, permissions={}", userId, roles, permissions);

        // 11. 生成新的 Refresh Token（Token 轮换，提升安全性）
        String newRefreshToken = RandomUtil.randomString(64);

        // 12. 将旧的 Refresh Token 加入黑名单（防止被继续使用）
        tokenService.addToBlacklist(refreshToken, 604800L);
        log.info("已将旧的 Refresh Token 加入黑名单：userId={}, device={}", userId, deviceType);

        // 13. 删除旧的 Refresh Token 及其反查 Key
        redisUtil.del(refreshKey);
        redisUtil.del(reverseKey);
        log.info("已删除旧的 Refresh Token：userId={}, device={}", userId, deviceType);

        // 14. 存储新的 Refresh Token（7天有效）
        redisUtil.set(refreshKey, newRefreshToken, 604800);
        String newReverseKey = "refresh_token:" + newRefreshToken;
        redisUtil.set(newReverseKey, userId + ":" + deviceType, 604800);
        log.info("已生成新的 Refresh Token：userId={}, device={}", userId, deviceType);

        // 15. 记录刷新日志
        log.info("Token 刷新成功：userId={}, device={}", userId, deviceType);

        // 16. 返回新的 Access Token 和新的 Refresh Token
        return TokenVO.builder()
                .accessToken(newAccessToken)
                .refreshToken(newRefreshToken)  // 返回新的 Refresh Token
                .accessTokenTimeout(tokenInfo.getTokenTimeout())
                .refreshTokenTimeout(604800L)
                .loginDevice(deviceType)
                .build();
    }

    @Override
    public void logout() {
        // 1. 获取当前用户信息
        Long userId = StpUtil.getLoginIdAsLong();
        String deviceType = StpUtil.getLoginDevice();
        String accessToken = StpUtil.getTokenValue();

        // 2. 将 Access Token 加入黑名单（剩余有效时间）
        long tokenTimeout = StpUtil.getTokenTimeout();
        if (tokenTimeout > 0) {
            tokenService.addToBlacklist(accessToken, tokenTimeout);
            log.info("已将 Access Token 加入黑名单：userId={}, device={}, timeout={}s", userId, deviceType, tokenTimeout);
        }

        // 3. 获取并删除 Refresh Token
        String refreshKey = RedisKeys.buildRefreshTokenKey(userId, deviceType);
        String storedRefreshToken = redisUtil.get(refreshKey);
        
        if (storedRefreshToken != null) {
            // 将 Refresh Token 加入黑名单（7天）
            tokenService.addToBlacklist(storedRefreshToken, 604800L);
            log.info("已将 Refresh Token 加入黑名单：userId={}, device={}", userId, deviceType);
            
            // 删除 Refresh Token 及其反查 Key
            String reverseKey = "refresh_token:" + storedRefreshToken;
            redisUtil.del(reverseKey);
            redisUtil.del(refreshKey);
        }

        // 4. Sa-Token 注销当前设备
        StpUtil.logout(userId, deviceType);

        log.info("用户注销成功：userId={}, device={}", userId, deviceType);
    }

    @Override
    public void logoutAll() {
        // 1. 获取用户ID
        Long userId = StpUtil.getLoginIdAsLong();

        // 2. 获取所有设备的 Access Token 并加入黑名单
        List<String> tokens = StpUtil.getTokenValueListByLoginId(userId);
        for (String token : tokens) {
            try {
                // 获取每个 Token 的剩余有效时间
                long timeout = StpUtil.getTokenTimeout(token);
                if (timeout > 0) {
                    tokenService.addToBlacklist(token, timeout);
                    log.info("已将 Access Token 加入黑名单：userId={}, token={}, timeout={}s", userId, token, timeout);
                }
            } catch (Exception e) {
                log.warn("获取 Token 超时时间失败，使用默认值：token={}", token);
                tokenService.addToBlacklist(token, 1800L);
            }
        }

        // 3. 删除所有设备的 Refresh Token 并加入黑名单
        String[] devices = {"WEB", "APP", "MINI"};
        for (String device : devices) {
            String refreshKey = RedisKeys.buildRefreshTokenKey(userId, device);
            String storedRefreshToken = redisUtil.get(refreshKey);
            
            if (storedRefreshToken != null) {
                // 将 Refresh Token 加入黑名单（7天）
                tokenService.addToBlacklist(storedRefreshToken, 604800L);
                log.info("已将 Refresh Token 加入黑名单：userId={}, device={}", userId, device);
                
                // 删除 Refresh Token 及其反查 Key
                String reverseKey = "refresh_token:" + storedRefreshToken;
                redisUtil.del(reverseKey);
                redisUtil.del(refreshKey);
            }
        }

        // 4. Sa-Token 注销所有设备
        StpUtil.logout(userId);

        log.info("用户全设备注销成功：userId={}", userId);
    }
}
